# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import _
from odoo.tools import OrderedSet
from collections import defaultdict, OrderedDict


import io
from odoo.tools import pycompat
from odoo.addons.web.controllers.main import ExportXlsxWriter
import base64

# image_process, topological_sort, html_escape, , ustr, apply_inheritance_specs, lazy_property


def csv_from_data(fields, rows):
    fp = io.BytesIO()
    writer = pycompat.csv_writer(fp, quoting=1)

    writer.writerow(fields)

    for data in rows:
        row = []
        for d in data:
            # Spreadsheet apps tend to detect formulas on leading =, + and -
            if isinstance(d, str) and d.startswith(('=', '-', '+')):
                d = "'" + d

            row.append(pycompat.to_text(d))
        writer.writerow(row)

    return fp.getvalue()


def xls_from_data(fields, rows):
    with ExportXlsxWriter(fields, len(rows)) as xlsx_writer:
        for row_index, row in enumerate(rows):
            for cell_index, cell_value in enumerate(row):
                if isinstance(cell_value, (list, tuple)):
                    cell_value = pycompat.to_text(cell_value)
                xlsx_writer.write_cell(row_index + 1, cell_index, cell_value)

    return xlsx_writer.value


def export_base64_from_data(fields, export_data, file_extension):
    if file_extension in ['xlsx', 'xls']:
        raw = xls_from_data(fields, export_data)
        file_type = 'xlsx'
    else:
        raw = csv_from_data(fields, export_data)
        file_type = 'csv'

    base64str = str(base64.b64encode(raw), encoding='utf8')

    return {
        'extension': file_type,
        'base64str': base64str
    }


Operators = {
    'eq':  '=',   # 默认为 '='  if value is integer
    'neq': '!=',
    'gt':  '>',
    'ge':  '>=',
    'lt':  '<',
    'le':  '<=',
    'eq_null':   '=?',
    'eq_like':   '=like',
    'like':      'like',
    'not_like':  'not like',
    'ilike':     'ilike',
    'not_ilike': 'not ilike',
    'eq_ilike':  '=ilike',
    'in':        'in',         # 默认为 'in' if value is list
    'not_in':    'not in',
    'child_of':  'child_of',
    'parent_of': 'parent_of'
}


class Model2(object):
    """
    TBD: 2019-7-20, obj 需要 with_context(context)

    所有的 fields 参数格式为嵌套的 {{},{}}。同时支持简单的[list]，

    2019-11-27
    简单的 domain, 都是 与 关系的, 可以用 dict 传参数, 前端 使用 dict 这种结构易理解,
    复杂的 domain, 有 或 非 关系的, dict 不容易设计, 依然可以用 list 格式，

    {
        # 该格式约定, 等同于 [id, 'in', [int] ]，         常用于 ids 过滤
        id:          [int],
        # 该格式约定, 等同于 [partner_id, '=', int ]，    常用于 id 过滤
        partner_id:  int,
        # 该格式约定, 等同于 [name, 'like', string ]，    常用于 模糊查询
        name___like: 'string',
        # 该格式约定, 等同于 [date, '>',  'yyyy-mm-dd' ]，常用于 时间范围查询
        date___gt:   'yyyy-mm-dd',
        # 该格式约定, 等同于 [date, '>=', 'yyyy-mm-dd' ]，常用于 时间范围查询
        date___ge:   'yyyy-mm-dd',
        # 该格式约定, 等同于 [date, '<',  'yyyy-mm-dd' ]，常用于 时间范围查询
        date___lt:   'yyyy-mm-dd',
        # 该格式约定, 等同于 [date, '<=', 'yyyy-mm-dd' ]，常用于 时间范围查询
        date___le:   'yyyy-mm-dd',
    }

    """
    @classmethod
    def test_function(cls, obj, rid, *args, **kwargs):
        print('xxxx,123456789, test_function', obj._name, rid)

    @classmethod
    def test_function2(cls, obj, rid, *args, **kwargs):
        print('xxxx,123456789, test_function', obj._name, rid)

        company_id = obj.env.user.company_id.id
        print('xxxx,123456789, test_function, company_id', obj._name, company_id)

        metadata = obj.fields_get(['journal_id'])
        print('xxxx,123456789, test_function, metadata ',
              obj._name, 'journal_id', metadata)
        domain = metadata['journal_id']['domain']
        print('xxxx,123456789, test_function, domain ',
              obj._name, domain, type(domain))
        domain2 = eval(domain, {"company_id": company_id})
        print('xxxx,123456789, test_function, domain2 ',
              obj._name, domain2, type(domain2))

        # ss = cls.get_options(obj, {'journal_id': {} } )
        # print('xxxx,123456789, test_function, ss ', obj._name, 'journal_id', ss)

        return {}

    @classmethod
    def _domain_dict2list(cls, domain, domain2):
        # domain, domain2 两个参数, 分别传 list 和 dict 参数

        if not domain:
            domain = []

        if not domain2:
            domain2 = []

        def fn(field_op, value):
            field_op_list = field_op.split('___')
            if len(field_op_list) == 1:
                field_op_list = field_op_list + \
                    [isinstance(value, list) and 'in' or 'eq']

            field, op = field_op_list
            op = Operators[op]
            return field, op, value

        if isinstance(domain, dict):
            domain = [fn(field_op, value)
                      for field_op, value in domain.items()]

        elif not isinstance(domain, list):
            # raise error
            domain = []

        if isinstance(domain2, dict):
            domain2 = [fn(field_op, value)
                       for field_op, value in domain2.items()]

        elif not isinstance(domain2, list):
            # raise error
            domain2 = []

        return domain + domain2

    @classmethod
    def export_base64(cls, obj, ids, fields, file_extension=None,  *args, **kwargs):
        if not file_extension:
            file_extension = 'csv'
        if file_extension not in ['csv', 'xlsx', 'xls']:
            file_extension = 'csv'

        if isinstance(fields, dict):
            fields = list(fields.keys())

        if not isinstance(fields, list):
            fields = ['display_name']

        if not fields:
            fields = ['display_name']

        records = obj.browse(ids)
        export_data = records.export_data(fields).get('datas', [])

        response_data = export_base64_from_data(
            fields, export_data, file_extension)

        filename = '_'.join(obj._name.split('.'))
        res = {'file_name': filename + '.' + file_extension}
        res.update(response_data)
        return res

    # TBD 这个现在有问题
    @classmethod
    def onchange(cls, obj, ids, values, field_name, field_onchange=None,
                 values_m2o=None, field_onchange_o2m=None, *args, **kwargs):
        # ids, values, field_name, field_onchange, 这是原装参数
        # field_onchange,可none,通过 fields_get 补全, 但是, o2m字段 没法补
        # values, create时, 有默认值补全
        # values, write时, 可以通过 browse(ids)补全, 但是 父表的 没法补
        # values, create时, 有些 number类型的字段需要补0  ,父表也需要 补全
        # 因此, 扩展两个字段 分别表示这两个补全的字段
        # field_onchange_o2m, 表示 field_onchange 需要补全的 o2m 字段
        # values_m2o,  表示 values 需要补全的 m2o 字段
        #
        fs_get = obj.fields_get([], ['type', 'relation'])
        if not field_onchange:
            field_onchange = dict((fld, '1') for fld in fs_get)
            # 补充 o2m 子表 的字段

            if not field_onchange_o2m:
                field_onchange_o2m = []
            if not isinstance(field_onchange_o2m, list):
                field_onchange_o2m = [field_onchange_o2m]

            if field_onchange_o2m:
                for fld_onchg_o2m in field_onchange_o2m:
                    o2m_obj = getattr(obj, fld_onchg_o2m)
                    o2m_fs_get = o2m_obj.fields_get([], ['type'])
                    o2m_field_onchange = dict(
                        ('%s.%s' % (fld_onchg_o2m, fld), '1') for fld in o2m_fs_get)
                    field_onchange.update(o2m_field_onchange)

        # print('onchange, ids, values, field_name, field_onchange,',
        #       ids, values, field_name, field_onchange)

        # 没有 browse(ids), 为什么 起作用了
        if(ids):
            obj = obj.browse(ids)
        # 没有 browse(ids), 为什么 起作用了

        # values 如果不全, 那么返回值, 会有问题
        # create 时 , 因为先取 default_get, values 是全的.
        # write 时, values 不全, 这里应该 先browse 补全一下

        def fn(obj, fld, meta):
            v = getattr(obj, fld)
            if meta['type'] == 'many2one':
                v = v.id
            elif meta['type'] in ['one2many', 'many2many']:
                v = []
                # o2m 字段 在 onchange 里的 格式 是 [[0,_,{}]] 这种
                # 貌似 不需要补充 o2m , 应该没问题. 2020-10-11
            return v

        values1 = dict((fld, fn(obj, fld, meta))
                       for fld, meta in fs_get.items())

        values1.update(values)

        if values_m2o:
            m2o_obj = getattr(obj, values_m2o)
            if not ids:
                m2o_id = values.get(values_m2o, {}).get('id')
                if m2o_id:
                    m2o_obj = m2o_obj.browse(m2o_id)

            m2o_fields = m2o_obj.fields_get([], ['type'])
            m2o_val = dict((fld, fn(m2o_obj, fld, meta))
                           for fld, meta in m2o_fields.items())
            m2o_val.update(values1[values_m2o] or {})
            values1[values_m2o] = m2o_val

        # print('onchange,2,', values1)

        onchange_ret = obj.onchange(values1, field_name, field_onchange)
        # print('onchange,field_onchange,2,', onchange_ret)

        # 返回值,  处理 m2o 字段
        value = onchange_ret.get('value', {})
        value2 = {}
        for k, v in value.items():
            # ['many2one', 'char', 'text', 'html', 'date', 'datetime']
            # if fs_get.get(k, {}).get('type') == 'date':
            if fs_get.get(k, {}).get('type') in ['char', 'text', 'html', 'date', 'datetime']:
                value2[k] = v or None
                continue

            if fs_get.get(k, {}).get('type') not in ['many2one', 'one2many', 'many2many']:
                value2[k] = v
                continue

            if fs_get.get(k, {}).get('type') == 'many2one':
                # m2o_id, m2o_name = v or [None, None]
                m2o_id = v and v[0] or None
                value2[k] = m2o_id
                # value2[k + '__name'] = m2o_name
                continue

            # in ['one2many', 'many2many']:
            value2[k] = v
            if not value[k]:
                continue
            o2m_model = fs_get.get(k, {}).get('relation')
            if not o2m_model:
                continue

            o2m_fs_get = obj.env[o2m_model].fields_get([], ['type'])

            for o2m in value2[k]:
                if not o2m:
                    continue
                if len(o2m) < 3:
                    continue
                if not isinstance(o2m[2], dict):
                    continue

                o2m_vals = o2m[2]
                o2m_vals2 = {}

                for o2m_key, o2m_v in o2m_vals.items():
                    if o2m_fs_get.get(o2m_key, {}).get('type') != 'many2one':
                        o2m_vals2[o2m_key] = o2m_v
                        continue

                    # o2m_m2o_id, o2m_m2o_name = o2m_v or [None, None]
                    o2m_m2o_id = o2m_v and o2m_v[0] or None
                    o2m_vals2[o2m_key] = o2m_m2o_id
                    # o2m_vals2[o2m_key + '__name'] = o2m_m2o_name

                o2m_vals.update(o2m_vals2)

        onchange_ret.update({'value': value2})

        return onchange_ret

    # TBD 这个现在有问题
    @classmethod
    def default_get(cls, obj, fields=None, context=None, *args, **kwargs):
        fs_get = obj.fields_get(attributes=['type'])
        if not fields:
            fields = fs_get

        fields = list(fields.keys())
        res = obj.default_get(fields)

        def fn(k, v):
            if fs_get[k]['type'] in ['many2one', 'char', 'text', 'html', 'date', 'datetime']:
                return v or None
            else:
                return v

            # if fs_get[k]['type'] == 'date' and not v:
            #     return None
            # if fs_get[k]['type'] == 'many2one' and not v:
            #     return None
            # return v

        res = dict((k, fn(k, v)) for k, v in res.items())

        return res

    @classmethod
    def fields_get(cls, obj, fields=None, *args, **kwargs):
        # fields is dict, return all models
        # TBD
        return {}

    @classmethod
    def create(cls, obj, values_list, context=None, *args, **kwargs):
        if not context:
            context = {}

        context.update(obj._context)
        obj2 = obj.with_context(context)
        records = obj2.create(values_list)

        # for vals in values_list:
        #     rec = obj2.create(vals)
        #     cls.onchange(rec, rec.id)
        #     rec.write(vals)
        #     records = records | rec

        # # records = .create(values_list)
        # for rec in records:
        #     cls.onchange(rec, rec.id)
        #     rec.write()
        return records.ids

    @classmethod
    def write(cls, obj, rid, values, *args, **kwargs):
        record = obj.browse(rid)
        res = record.write(values)
        # cls.onchange(obj, rid)
        # res = record.write(values)
        return res

    @classmethod
    def search_count2(cls, obj, domain=None, domain2=None, context=None, *args, **kwargs):
        domain = cls._domain_dict2list(domain, domain2)
        return obj.search_count(domain)

    @classmethod
    def search_read1(cls, obj, domain=None,  domain2=None, fields=None,
                     offset=0, limit=None, order=None, context=None, *args, **kwargs):

        domain = cls._domain_dict2list(domain, domain2)
        recs = obj.search(domain, offset=offset, limit=limit, order=order)
        return cls._return_for_read1(recs, fields, context)

    @classmethod
    def search_read2(cls, obj, domain=None,  domain2=None, fields=None,
                     offset=0, limit=None, order=None, context=None, *args, **kwargs):

        domain = cls._domain_dict2list(domain, domain2)
        recs = obj.search(domain, offset=offset, limit=limit, order=order)
        return cls._return_for_read2(recs, fields, context)

    @classmethod
    def read1(cls, obj, ids, fields=None, context=None, *args, **kwargs):
        recs = obj.browse(ids)
        return cls._return_for_read1(recs, fields, context)

    @classmethod
    def read2(cls, obj, ids, fields=None, context=None, *args, **kwargs):
        recs = obj.browse(ids)
        return cls._return_for_read2(recs, fields, context)

    @classmethod
    def _create_1_and_2(cls, obj, values_list, recursive=None, fields=None, context=None, *args, **kwargs):
        rids = cls.create(obj, values_list, context=context)
        if not rids:
            return None

        if not fields:
            fields = ['id']
        if recursive:
            res = cls.read2(obj, rids, fields, context=context)
        else:
            res = cls.read1(obj, rids, fields, context=context)

        if isinstance(values_list, list):
            return res
        else:
            return res[0]

    @classmethod
    def create1(cls, *args, **kwargs):
        return cls._create_1_and_2(*args, **kwargs)

    @classmethod
    def create2(cls, *args, **kwargs):
        return cls._create_1_and_2(recursive=1, *args, **kwargs)

    @classmethod
    def _write_1_and_2(cls, obj, rid, values, recursive=None, fields=None, *args, **kwargs):
        res = cls.write(obj, rid, values)
        if not fields:
            fields = ['id']

        if recursive:
            res = cls.read2(obj, rid, fields)
        else:
            res = cls.read1(obj, rid, fields)

        if res:
            return res[0]

        return {}

    @classmethod
    def write1(cls, *args, **kwargs):
        return cls._write_1_and_2(*args, **kwargs)

    @classmethod
    def write2(cls, *args, **kwargs):
        return cls._write_1_and_2(recursive=1, *args, **kwargs)

    @classmethod
    def _get_meta(cls, obj, field, metadata=None,):

        fs = field.split('.')
        if len(fs) == 1:
            return metadata[field]

        elif len(fs) == 2:
            meta = metadata[fs[0]]
            if meta['type'] in ('many2one', 'one2many', 'many2many'):
                ref_metadata = obj.env[meta['relation']].fields_get([fs[1]])
                return ref_metadata[fs[1]]

        return {'type': None}

    # 2020-5-7 改过 返回值的格式, 以前的东西会有问题
    # 现在的返回值 :  for selection, [{value, lable}]
    # 现在的返回值 :  for m2m/o2m/m2o, [{value, lable, object}]
    # 以前的返回值 :  for selection, [{value, name}]
    # 以前的返回值 :  for m2m/o2m/m2o, [{...object}]
    @classmethod
    def _get_option(cls, obj, field, metadata=None,
                    domain=None, domain2=None,
                    fields=None, order=None, cascader=None):

        if not metadata:
            metadata = {}

        meta = cls._get_meta(obj, field, metadata)

        if not meta:
            return []

        if meta['type'] == 'selection':
            return [{'value': sel[0], 'text': sel[1], 'label': sel[1]
                     } for sel in meta['selection']]

        elif meta['type'] in ['many2one', 'many2many', 'one2many']:
            ref = obj.env[meta['relation']]
            fields = fields or ['display_name']

            if 'display_name' not in fields:
                if isinstance(fields, dict):
                    fields['display_name'] = None
                elif isinstance(fields, list):
                    fields = fields + ['display_name']

            # TBD 2020-1-9,  需要根据实际情况不断补充完善
            my_globals = {"company_id": obj.env.user.company_id.id}
            domain_default = meta.get('domain', [])
            if isinstance(domain_default, str):
                try:
                    domain_default = eval(domain_default, my_globals)
                except:
                    pass

            # TBD 2020-1-9,  需要根据实际情况不断补充完善
            if not isinstance(domain_default, (list, tuple)):
                domain_default = []

            domain3 = cls._domain_dict2list(domain, domain2)
            domain3 = domain_default + domain3
            records = cls.search_read2(
                ref, domain=domain3, domain2={}, fields=fields, order=order)

            records = [{
                'value': rec['id'],
                'text': rec['display_name'],
                'label': rec['display_name'],
                'object': rec
            } for rec in records]

            if not cascader:
                return records

            # TBD 2019-11-21, 根据 cascader 进行递归 处理
            # cascader: {
            #     value: 'id',
            #     label: 'display_name',
            #     children: {
            #         name: 'child_ids',
            #         cascader: {
            #             value: 'id',
            #             label: 'display_name'
            #         }
            #     }
            # }

            return records

        return []

    @classmethod
    def get_options(cls, obj, fields, *args, **kwargs):
        options = {}
        fs = [fld.split('.')[0] for fld in fields]
        metadata = obj.fields_get(fs)

        for fld, value in fields.items():
            if not value:
                value = {}

            options[fld] = cls._get_option(obj, fld, metadata, **value)

        return options

    # 暂时没用了, odoojs 中实现也很简单. 如果有必要, 可以重写.这里的代码是 老代码.
    @classmethod
    def wizard_submit(cls, obj, rid, wizard_model, wizard_id, vals, wizard_method,
                      *args, **kwargs):

        # wizard = obj.env[wizard_model].browse(wizard_id)
        # wizard.write(vals)

        # context = {}
        # context.update(obj._context)
        # context.update({
        #     'active_model': obj._name,
        #     'active_id': rid,
        #     'active_ids': [rid]
        # })

        # wizard = wizard.with_context(context)
        # return getattr(wizard, wizard_method)()
        pass

    # report wirard

    @classmethod
    def report_print(cls, obj, values, *args, **kwargs):
        # check 是否自定义了 print_report2
        # if ok, call 自定义的 print_report2, 并把 report_print1 作为参数传进去
        # else call report_print1
        if hasattr(obj, 'report_print'):
            return obj.report_print(values, cls.report_print1)
        else:
            return cls.report_print1(obj, values)

    @classmethod
    def report_print1(cls, obj, values, *args, **kwargs):
        wizard = obj.create(values)
        meta = wizard.check_report()

        report_name = 'report.%s' % meta['report_name']
        data = meta['data']
        context = {}
        context.update(meta.get('context', {}))
        context.update({
            'active_model': wizard._name,
            'active_id': wizard.id,
            'active_ids': [wizard.id]
        })

        report_obj = obj.env[report_name].with_context(context)

        fn = hasattr(report_obj, '_get_report_values2'
                     ) and report_obj._get_report_values2 or report_obj._get_report_values

        res = fn(None, data=data)
        return res

    # 2019-11-27
    # for selection field eg. 'state',      we set a new field 'state__name'
    # for many2one  field eg. 'partner_id', we set a new field 'partner_id__name'
    # for many2one  field eg. 'partner_id', we set a new field 'partner_id__onject' = {id, name, ...}
    # for many2one  field eg. 'partner_id', we set this  field 'partner_id' = id
    # for o2m m2m   field eg. 'line_ids',   we set a new field 'line_ids__names' = 'name1,name2,...'
    # for o2m m2m   field eg. 'line_ids',   we set a new field 'line_ids__objects' = [{id,name}]
    # for o2m m2m   field eg. 'line_ids',   we set this  field 'line_ids' = [ids]

    # 仅仅 处理 m2o 字段
    @classmethod
    def _return_for_read1(cls, obj, fields, context=None):
        return cls._return_for_read(obj, fields, context=context)

    # TBD, 读取 o2m 字段 嵌套 有 m2o 字段 会出现无限循环
    # 2019-12-23
    @classmethod
    def _return_for_read2(cls, obj, fields, context=None):
        return cls._return_for_read(obj, fields, context=context, recursive=1)

    @classmethod
    def _return_for_read(cls, obj, fields, context=None, recursive=None):
        if not fields:
            fields = {}

        if fields and isinstance(fields, list):
            fields = dict((f, None) for f in fields)

        if not context:
            context = {}

        if not context.get('return_for_test'):
            # 设置 context.return_for_test 且 设置 fields = None
            # 则 用于测试 可以返回 所有的 字段
            # 不起作用, 需要测试. TBD
            fields['display_name'] = None

        # fn2 be copy from odoo.models.search_read(), 2018-8-13
        def fn2(records, fields11):
            result = records.read(fields11)
            if len(result) <= 1:
                return result

            # reorder read
            index = {vals['id']: vals for vals in result}
            return [index[record.id] for record in records if record.id in index]

        records = fn2(obj, fields.keys())

        metadata = obj.fields_get(fields.keys())

        def fn3(field, ref_fields):
            meta = metadata[field]
            if not meta:
                return None
            if meta['type'] in ('many2one'):
                ref_obj = obj.mapped(field)
                if recursive and ref_fields:
                    ref_recs = cls._return_for_read2(ref_obj, ref_fields)
                else:
                    ref_recs = []
                return ref_recs
            elif meta['type'] in ('one2many', 'many2many'):
                ref_obj = obj.mapped(field)
                if recursive:
                    ref_recs = cls._return_for_read2(ref_obj, ref_fields)
                else:
                    ref_recs = []
                return ref_recs

            else:
                return None

        ref_records = dict((field, fn3(field, ref_fields))
                           for field, ref_fields in fields.items())

        def fn_selection(rec, field, *args):
            meta = metadata[field]
            sel_val = rec[field] or None
            ret_rec = {}
            ret_rec[field] = sel_val
            ret_rec[field + '__name'] = sel_val and dict(
                meta['selection']).get(sel_val, sel_val) or None
            return ret_rec

        def fn_m2o(rec, field, *args):
            ref_val = rec[field]
            m2o_id, m2o_name = ref_val or [None, '']
            ret_rec = {}
            ret_rec[field] = m2o_id
            ret_rec[field + '__name'] = m2o_name
            return ret_rec

        def fn_m2o_recursive(rec, field, obj, ref_fields):
            ref_val = rec[field]
            m2o_id, m2o_name = ref_val or [None, '']

            ref_m2o = list(filter(
                lambda r: r['id'] == m2o_id, ref_records[field]))

            ref_m2o = ref_m2o and ref_m2o[0] or {}

            ret_rec = {}
            ret_rec[field] = m2o_id
            ret_rec[field + '__name'] = m2o_name
            ret_rec[field + '__object'] = ref_val and {
                'id': m2o_id,
                'name': m2o_name,
                'display_name': m2o_name} or {}

            ret_rec[field + '__object'].update(ref_m2o)

            return ret_rec

        def fn_m2m_recursive(rec, field, obj, ref_fields):
            ref_val = rec[field]
            m2m_ids = ref_val or []

            ref_m2m = list(filter(
                lambda r: r['id'] in m2m_ids, ref_records[field]))

            ret_rec = {}
            ret_rec[field + '__count'] = len(m2m_ids)
            ret_rec[field + '__objects'] = ref_m2m
            ret_rec[field + '__names'] = ','.join(
                [m2m['display_name'] for m2m in ref_m2m])

            return ret_rec

        def fn_text(rec, field, *args):
            ret_rec = {}
            sel_val = rec[field]
            ret_rec[field] = sel_val or None
            return ret_rec

        def fn4(field):
            meta = metadata[field]
            if not meta:
                return None

            if meta['type'] == 'selection':
                return fn_selection
            elif meta['type'] in ('many2one'):

                return recursive and fn_m2o_recursive or fn_m2o
            elif meta['type'] in ('one2many', 'many2many'):
                return recursive and fn_m2m_recursive or None

            elif meta['type'] in ['char', 'text', 'html', 'date', 'datetime']:
                return fn_text
            else:
                return None

        meta_fn = dict((field, fn4(field)) for field in fields.keys())

        for rec in records:
            for field, ref_fields in fields.items():
                fn = meta_fn[field]
                if fn:
                    rec.update(fn(rec, field, obj, ref_fields))

        return records

# # to DEL 2020-9-24

# #


#     @classmethod
#     def wizard_create(cls, obj, rid, model, vals, fields=None, context=None, *args, **kwargs):

#         # if not fields:
#         #     fields = []
#         # if not context:
#         #     context = {}

#         # # context 是字符串, 需要转为对象
#         # if isinstance(context, str):
#         #     # 这段代码是从哪儿抄过来的? To be check, 2020-05-30
#         #     my_globals = {"ref": obj.env['ir.model.data'].xmlid_to_res_id}
#         #     context = eval(context, my_globals)

#         # context.update(obj._context)
#         # context.update({
#         #     'active_model': obj._name,
#         #     'active_id': rid,
#         #     'active_ids': [rid]
#         # })

#         # print('12345678, context,', context)

#         # wizard = obj.env[model].with_context(context)

#         # wizard = wizard.create(vals)

#         # if fields:
#         #     context = {}
#         # else:
#         #     context = {'return_for_test': 1}

#         # return cls._return_for_read2(wizard, fields, context)[0]

#         pass

#     @classmethod
#     def report_print_no_used_22222222(cls, obj, wizard_model, wizard_id, wizard_values,
#                                       fields=None, context=None, *args, **kwargs):
#         wizard = obj.env[wizard_model]
#         wizard = wizard.browse(wizard_id)
#         if wizard_values:
#             wizard.write(wizard_values)

#         if not fields:
#             fields = {}
#         wizard_data = cls._return_for_read2(wizard, fields)[0]

#         report_meta = wizard.check_report()

#         if not context:
#             context = {}

#         context.update(obj._context)
#         context.update(report_meta.get('context', {}))
#         context.update({
#             'active_model': wizard_model,
#             'active_id': wizard_id,
#             'active_ids': [wizard_id]
#         })

#         obj = obj.with_context(context)
#         fn = hasattr(obj, '_get_report_values2'
#                      ) and obj._get_report_values2 or obj._get_report_values

#         res = fn(None, data=report_meta.get('data', None))

#         res.update({
#             'wizard_data': wizard_data
#         })

#         return res

#     # after create or write, onchange is not trigger
#     # so call this onchange, to update other fields
#     # this is copy ftom odoo.models.onchange
#     # 2020-9-22
#     # 在 account.payment 中 , call onchange 出现问题了
#     # 因此 废弃这种方法. 改为     # 1 fields get, 2 onchange, 3 create
#     # 其他的 create/write 方法, 触发 onchange, 需要 一步步 在 odoo 页面中 查 network
#     @classmethod
#     def onchange_old_dellllll(cls, obj, rid, *args, **kwargs):
#         # print('xxxx,123456789, test_function', obj._name, rid )

#         def PrefixTree(model, dotnames):
#             """ Return a prefix tree for sequences of field names. """
#             if not dotnames:
#                 return {}
#             # group dotnames by prefix
#             suffixes = defaultdict(list)
#             for dotname in dotnames:
#                 # name, *names = dotname.split('.', 1)
#                 names = dotname.split('.', 1)
#                 name = names.pop(0)
#                 suffixes[name].extend(names)
#             # fill in prefix tree in fields order
#             tree = OrderedDict()
#             for name, field in model._fields.items():
#                 if name in suffixes:
#                     tree[name] = subtree = PrefixTree(
#                         model[name], suffixes[name])
#                     if subtree and field.type == 'one2many':
#                         subtree.pop(field.inverse_name, None)
#             return tree

#         class Snapshot(dict):
#             # """ A dict with the values of a record, following a prefix tree. """
#             __slots__ = ()

#             def __init__(self, record, tree):
#                 # put record in dict to include it when comparing snapshots
#                 super(Snapshot, self).__init__(
#                     {'<record>': record, '<tree>': tree})
#                 for name in tree:
#                     self.fetch(name)

#             def fetch(self, name):
#                 # """ Set the value of field ``name`` from the record's value. """
#                 record = self['<record>']
#                 tree = self['<tree>']
#                 if record._fields[name].type in ('one2many', 'many2many'):
#                     # x2many fields are serialized as a list of line snapshots
#                     self[name] = [Snapshot(line, tree[name])
#                                   for line in record[name]]
#                 else:
#                     self[name] = record[name]

#             def has_changed(self, name):
#                 # """ Return whether a field on record has changed. """
#                 record = self['<record>']
#                 subnames = self['<tree>'][name]
#                 if record._fields[name].type not in ('one2many', 'many2many'):
#                     return self[name] != record[name]
#                 return (
#                     len(self[name]) != len(record[name])
#                     or (
#                         set(line_snapshot["<record>"].id for line_snapshot in self[name])
#                         != set(record[name]._ids)
#                     )
#                     or any(
#                         line_snapshot.has_changed(subname)
#                         for line_snapshot in self[name]
#                         for subname in subnames
#                     )
#                 )

#             def diff(self, other):
#                 # """ Return the values in ``self`` that differ from ``other``.
#                 #     Requires record cache invalidation for correct output!
#                 # """
#                 record = self['<record>']
#                 result = {}
#                 for name, subnames in self['<tree>'].items():
#                     if (name == 'id') or (other.get(name) == self[name]):
#                         continue
#                     field = record._fields[name]
#                     if field.type not in ('one2many', 'many2many'):
#                         result[name] = field.convert_to_onchange(
#                             self[name], record, {})
#                     else:
#                         # x2many fields: serialize value as commands
#                         result[name] = commands = [(5,)]
#                         for line_snapshot in self[name]:
#                             line = line_snapshot['<record>']
#                             line = line._origin or line
#                             if not line.id:
#                                 # new line: send diff from scratch
#                                 line_diff = line_snapshot.diff({})
#                                 commands.append(
#                                     (0, line.id.ref or 0, line_diff))
#                             else:
#                                 # existing line: check diff from database
#                                 # (requires a clean record cache!)
#                                 line_diff = line_snapshot.diff(
#                                     Snapshot(line, subnames))
#                                 if line_diff:
#                                     # send all fields because the web client
#                                     # might need them to evaluate modifiers
#                                     line_diff = line_snapshot.diff({})
#                                     commands.append((1, line.id, line_diff))
#                                 else:
#                                     commands.append((4, line.id))
#                 return result

#         field_onchange = obj._onchange_spec()
#         nametree = PrefixTree(obj.browse(), field_onchange)
#         # onchange_methods = obj._onchange_methods

#         record = obj.browse(rid)
#         snapshot0 = Snapshot(record, nametree)

#         result = {'warnings': OrderedSet()}

#         todo = list(nametree)
#         done = set()

#         # process names in order
#         while todo:
#             # print('xxxx,123456789, test_function, todo:', todo )
#             # apply field-specific onchange methods
#             for name in todo:
#                 if field_onchange.get(name):
#                     record._onchange_eval(name, field_onchange[name], result)
#                 done.add(name)

#             # determine which fields to process for the next pass
#             todo = [
#                 name
#                 for name in nametree
#                 if name not in done and snapshot0.has_changed(name)
#             ]

#             # if not env.context.get('recursive_onchanges', True):
#             #     todo = []

#         # format warnings
#         warnings = result.pop('warnings')
#         if len(warnings) == 1:
#             title, message, type = warnings.pop()
#             if not type:
#                 type = 'dialog'
#             result['warning'] = dict(title=title, message=message, type=type)
#         elif len(warnings) > 1:
#             # concatenate warning titles and messages
#             title = _("Warnings")
#             message = '\n\n'.join(
#                 [warn_title + '\n\n' + warn_message for warn_title, warn_message, warn_type in warnings])
#             result['warning'] = dict(
#                 title=title, message=message, type='dialog')

#         return result

# to DEL 2020-9-9


"""


    # TBD, 读取 o2m 字段 嵌套 有 m2o 字段 会出现无限循环
    # 2019-12-23
    @classmethod
    def _return_for_read2_old(cls, obj, fields, context=None):
        # print('123456789,init, obj,', obj)
        # print('123456789,init, fields,', fields)

        if not fields:
            fields = {}

        if fields and isinstance(fields, list):
            fields = dict((f, None) for f in fields)

        if not context:
            context = {}

        if not context.get('return_for_test'):
            # 设置 context.return_for_test 且 设置 fields = None
            # 则 用于测试 可以返回 所有的 字段
            fields['display_name'] = None

        # fn2 be copy from odoo.models.search_read(), 2018-8-13
        def fn2(records, fields11):
            result = records.read(fields11)
            if len(result) <= 1:
                return result

            # reorder read
            index = {vals['id']: vals for vals in result}
            return [index[record.id] for record in records if record.id in index]

        records = fn2(obj, fields.keys())

        metadata = obj.fields_get(fields.keys())

        # TBD, 2019-12-25, fields 为空时 , 回所有字段 而且是 不处理的.

        for rec in records:
            for field, ref_fields in fields.items():
                meta = metadata[field]
                if not meta:
                    continue

                if meta['type'] == 'selection':
                    sel_val = rec[field] or None
                    rec[field] = sel_val

                    if sel_val:
                        rec[field +
                            '__name'] = dict(meta['selection']).get(sel_val, sel_val)
                    else:
                        rec[field + '__name'] = sel_val

                elif meta['type'] in ('many2one', 'one2many', 'many2many'):
                    ref_val = rec[field]
                    if meta['type'] == 'many2one':
                        m2o_id, m2o_name = ref_val or [None, '']

                        rec[field] = m2o_id
                        rec[field + '__name'] = m2o_name
                        rec[field + '__object'] = ref_val and {
                            'id': m2o_id,
                            'name': m2o_name,
                            'display_name': m2o_name} or {}

                        if ref_val and ref_fields:
                            obj_one = obj.filtered(lambda r: r.id == rec['id'])
                            ref_obj = getattr(obj_one, field)

                            ref_m2o = cls._return_for_read2(
                                ref_obj, ref_fields)
                            ref_m2o = ref_m2o and ref_m2o[0] or {}
                            rec[field + '__object'].update(ref_m2o)

                    else:
                        m2m_ids = ref_val or []
                        rec[field + '__count'] = len(m2m_ids)

                        # if not ref_fields:
                        #     ref_fields = {
                        #         'display_name': None
                        #     }

                        obj_one = obj.filtered(lambda r: r.id == rec['id'])
                        ref_obj = getattr(obj_one, field)

                        # print('123456789,ref_obj,', ref_obj)
                        # print('123456789,ref_fields,', ref_fields)
                        ref_records = cls._return_for_read2(
                            ref_obj, ref_fields)
                        # print('123456789,ref_records,', ref_records)
                        rec[field + '__objects'] = ref_records
                        rec[field + '__names'] = ','.join(
                            [m2m['display_name'] for m2m in ref_records])

                        # if ref_fields:
                        #     ref_fields['display_name'] = None

                        #     if no_read:
                        #         ref_obj = ref_val
                        #     else:
                        #         obj_one = obj.filtered(lambda r: r.id == rec['id'] )
                        #         ref_obj = getattr(obj_one, field)

                        #     ref_records = cls._return_for_read2(ref_obj, ref_fields, no_read=no_read)
                        #     rec[ field + '__objects' ] = ref_records
                        #     rec[ field + '__names' ] = ','.join([ m2m['display_name'] for m2m in ref_records] )

                        # else:
                        #     rec[ field + '__names' ] = ', '.join([ '(%s,%d)' % (meta['relation'], rid ) for rid in  m2m_ids] )
                        #     rec[ field + '__objects' ] = [ {'id': m2m_id} for m2m_id in m2m_ids ]

                elif meta['type'] in ['char', 'text', 'html', 'date', 'datetime']:
                    sel_val = rec[field]
                    rec[field] = sel_val or None

        return records

    # TBD, 读取 o2m 字段 嵌套 有 m2o 字段 会出现无限循环
    # 2019-12-23
    @classmethod
    def _return_for_read2_____old(cls, obj, fields, context=None, no_read=None):
        # print('123456789,init, obj,', obj)
        # print('123456789,init, fields,', fields)
        # no_read 是做什么的? TBD 2020-9-9

        if not fields:
            fields = {}

        if fields and isinstance(fields, list):
            fields = dict((f, None) for f in fields)

        if not context:
            context = {}

        if not context.get('return_for_test'):
            # 设置 context.return_for_test 且 设置 fields = None
            # 则 用于测试 可以返回 所有的 字段
            fields['display_name'] = None

        # fn2 be copy from odoo.models.search_read(), 2018-8-13
        def fn2(records, fields11):
            # result = records.read(fields11)
            result = []
            if no_read:
                fields2 = list(fields11)
                if not fields2:
                    fields2 = list(obj.fields_get().keys())

                if 'id' not in fields2:
                    fields2 = ['id'] + fields2

                # # 调用 new 函数时, account.move 的 string_to_hash 字段 计算函数 出错
                # if no_read and obj._name == 'account.move':
                #     fields2 = [fld for fld in fields2 if fld !=
                #                'string_to_hash']

                def fn222(rec, fld):
                    # print('xxxxxxx, fld', fld)
                    return getattr(rec, fld)

                result = [dict((fld, fn222(rec, fld)) for fld in fields2)
                          for rec in records]

                # result = [ dict( (fld, getattr(rec, fld) ) for fld in fields2 )
                #      for rec in records]

            else:
                result = records.read(fields11)

            if len(result) <= 1:
                return result

            # reorder read
            index = {vals['id']: vals for vals in result}
            return [index[record.id] for record in records if record.id in index]

        records = fn2(obj, fields.keys())

        metadata = obj.fields_get(fields.keys())

        # TBD, 2019-12-25, fields 为空时 , 回所有字段 而且是 不处理的.

        for rec in records:
            for field, ref_fields in fields.items():
                meta = metadata[field]
                if not meta:
                    continue

                if meta['type'] == 'selection':
                    sel_val = rec[field] or None
                    rec[field] = sel_val

                    if sel_val:
                        rec[field +
                            '__name'] = dict(meta['selection']).get(sel_val, sel_val)
                    else:
                        rec[field + '__name'] = sel_val

                elif meta['type'] in ('many2one', 'one2many', 'many2many'):
                    ref_val = rec[field]
                    if meta['type'] == 'many2one':
                        if no_read:
                            m2o_id, m2o_name = ref_val.id, ref_val.display_name
                        else:
                            m2o_id, m2o_name = ref_val or [None, '']

                        rec[field] = m2o_id
                        rec[field + '__name'] = m2o_name
                        rec[field + '__object'] = ref_val and {
                            'id': m2o_id,
                            'name': m2o_name,
                            'display_name': m2o_name} or {}

                        if ref_val and ref_fields:
                            if no_read:
                                ref_obj = ref_val
                            else:
                                obj_one = obj.filtered(
                                    lambda r: r.id == rec['id'])
                                ref_obj = getattr(obj_one, field)

                            ref_m2o = cls._return_for_read2(
                                ref_obj, ref_fields, no_read=no_read)
                            ref_m2o = ref_m2o and ref_m2o[0] or {}
                            rec[field + '__object'].update(ref_m2o)

                    else:
                        if no_read:
                            print('xxxxxxxx, m2m, ', field, ref_val,
                                  type(ref_val), ref_val._ids)
                            m2m_ids = ref_val._ids
                        else:
                            m2m_ids = ref_val or []

                        rec[field + '__count'] = len(m2m_ids)

                        # if not ref_fields:
                        #     ref_fields = {
                        #         'display_name': None
                        #     }

                        if no_read:
                            ref_obj = ref_val
                        else:
                            obj_one = obj.filtered(lambda r: r.id == rec['id'])
                            ref_obj = getattr(obj_one, field)

                        # print('123456789,ref_obj,', ref_obj)
                        # print('123456789,ref_fields,', ref_fields)
                        ref_records = cls._return_for_read2(
                            ref_obj, ref_fields, no_read=no_read)
                        # print('123456789,ref_records,', ref_records)
                        rec[field + '__objects'] = ref_records
                        rec[field + '__names'] = ','.join(
                            [m2m['display_name'] for m2m in ref_records])

                        # if ref_fields:
                        #     ref_fields['display_name'] = None

                        #     if no_read:
                        #         ref_obj = ref_val
                        #     else:
                        #         obj_one = obj.filtered(lambda r: r.id == rec['id'] )
                        #         ref_obj = getattr(obj_one, field)

                        #     ref_records = cls._return_for_read2(ref_obj, ref_fields, no_read=no_read)
                        #     rec[ field + '__objects' ] = ref_records
                        #     rec[ field + '__names' ] = ','.join([ m2m['display_name'] for m2m in ref_records] )

                        # else:
                        #     rec[ field + '__names' ] = ', '.join([ '(%s,%d)' % (meta['relation'], rid ) for rid in  m2m_ids] )
                        #     rec[ field + '__objects' ] = [ {'id': m2m_id} for m2m_id in m2m_ids ]

                elif meta['type'] in ['char', 'text', 'html', 'date', 'datetime']:
                    sel_val = rec[field]
                    rec[field] = sel_val or None

        return records

"""
